/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.log4j.xml;

import org.apache.log4j.Appender;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.config.PropertySetter;
import org.apache.log4j.helpers.FileWatchdog;
import org.apache.log4j.helpers.Loader;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.or.RendererMap;
import org.apache.log4j.spi.AppenderAttachable;
import org.apache.log4j.spi.Configurator;
import org.apache.log4j.spi.ErrorHandler;
import org.apache.log4j.spi.Filter;
import org.apache.log4j.spi.LoggerFactory;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.spi.OptionHandler;
import org.apache.log4j.spi.RendererSupport;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Hashtable;
import java.util.Properties;

// Contributors:   Mark Womack
//                 Arun Katkere 

/**
 * Use this class to initialize the log4j environment using a DOM tree.
 * 
 * <p>
 * The DTD is specified in <a href="log4j.dtd"><b>log4j.dtd</b></a>.
 * 
 * <p>
 * Sometimes it is useful to see how log4j is reading configuration files. You
 * can enable log4j internal logging by defining the <b>log4j.debug</b> variable
 * on the java command line. Alternatively, set the <code>debug</code> attribute
 * in the <code>log4j:configuration</code> element. As in
 * 
 * <pre>
 * &lt;log4j:configuration &lt;b&gt;debug=&quot;true&quot;&lt;/b&gt; xmlns:log4j=&quot;http://jakarta.apache.org/log4j/&quot;&gt;
 *    ...
 *    &lt;/log4j:configuration&gt;
 * </pre>
 * 
 * <p>
 * There are sample XML files included in the package.
 * 
 * @author Christopher Taylor
 * @author Ceki G&uuml;lc&uuml;
 * @author Anders Kristensen
 * @since 0.8.3
 */
public class DOMConfigurator implements Configurator {

	static final String CONFIGURATION_TAG = "log4j:configuration";
	static final String OLD_CONFIGURATION_TAG = "configuration";
	static final String RENDERER_TAG = "renderer";
	static final String APPENDER_TAG = "appender";
	static final String APPENDER_REF_TAG = "appender-ref";
	static final String PARAM_TAG = "param";
	static final String LAYOUT_TAG = "layout";
	static final String CATEGORY = "category";
	static final String LOGGER = "logger";
	static final String LOGGER_REF = "logger-ref";
	static final String CATEGORY_FACTORY_TAG = "categoryFactory";
	static final String LOGGER_FACTORY_TAG = "loggerFactory";
	static final String NAME_ATTR = "name";
	static final String CLASS_ATTR = "class";
	static final String VALUE_ATTR = "value";
	static final String ROOT_TAG = "root";
	static final String ROOT_REF = "root-ref";
	static final String LEVEL_TAG = "level";
	static final String PRIORITY_TAG = "priority";
	static final String FILTER_TAG = "filter";
	static final String ERROR_HANDLER_TAG = "errorHandler";
	static final String REF_ATTR = "ref";
	static final String ADDITIVITY_ATTR = "additivity";
	static final String THRESHOLD_ATTR = "threshold";
	static final String CONFIG_DEBUG_ATTR = "configDebug";
	static final String INTERNAL_DEBUG_ATTR = "debug";
	private static final String RESET_ATTR = "reset";
	static final String RENDERING_CLASS_ATTR = "renderingClass";
	static final String RENDERED_CLASS_ATTR = "renderedClass";

	static final String EMPTY_STR = "";
	static final Class[] ONE_STRING_PARAM = new Class[]{String.class};

	final static String dbfKey = "javax.xml.parsers.DocumentBuilderFactory";

	// key: appenderName, value: appender
	Hashtable appenderBag;

	Properties props;
	LoggerRepository repository;

	protected LoggerFactory catFactory = null;

	/**
	 * No argument constructor.
	 */
	public DOMConfigurator() {
		appenderBag = new Hashtable();
	}

	/**
	 * Used internally to parse appenders by IDREF name.
	 */
	protected Appender findAppenderByName(Document doc, String appenderName) {
		Appender appender = (Appender) appenderBag.get(appenderName);

		if (appender != null) {
			return appender;
		} else {
			// Doesn't work on DOM Level 1 :
			// Element element = doc.getElementById(appenderName);

			// Endre's hack:
			Element element = null;
			NodeList list = doc.getElementsByTagName("appender");
			for (int t = 0; t < list.getLength(); t++) {
				Node node = list.item(t);
				NamedNodeMap map = node.getAttributes();
				Node attrNode = map.getNamedItem("name");
				if (appenderName.equals(attrNode.getNodeValue())) {
					element = (Element) node;
					break;
				}
			}
			// Hack finished.

			if (element == null) {
				LogLog.error("No appender named [" + appenderName
						+ "] could be found.");
				return null;
			} else {
				appender = parseAppender(element);
				appenderBag.put(appenderName, appender);
				return appender;
			}
		}
	}
	/**
	 * Used internally to parse appenders by IDREF element.
	 */
	protected Appender findAppenderByReference(Element appenderRef) {
		String appenderName = subst(appenderRef.getAttribute(REF_ATTR));
		Document doc = appenderRef.getOwnerDocument();
		return findAppenderByName(doc, appenderName);
	}

	/**
	 * Delegates unrecognized content to created instance if it supports
	 * UnrecognizedElementParser.
	 * 
	 * @since 1.2.15
	 * @param instance
	 *            instance, may be null.
	 * @param element
	 *            element, may not be null.
	 * @param props
	 *            properties
	 * @throws IOException
	 *             thrown if configuration of owner object should be abandoned.
	 */
	private static void parseUnrecognizedElement(final Object instance,
			final Element element, final Properties props) throws Exception {
		boolean recognized = false;
		if (instance instanceof UnrecognizedElementHandler) {
			recognized = ((UnrecognizedElementHandler) instance)
					.parseUnrecognizedElement(element, props);
		}
		if (!recognized) {
			LogLog.warn("Unrecognized element " + element.getNodeName());
		}
	}

	/**
	 * Delegates unrecognized content to created instance if it supports
	 * UnrecognizedElementParser and catches and logs any exception.
	 * 
	 * @since 1.2.15
	 * @param instance
	 *            instance, may be null.
	 * @param element
	 *            element, may not be null.
	 * @param props
	 *            properties
	 */
	private static void quietParseUnrecognizedElement(final Object instance,
			final Element element, final Properties props) {
		try {
			parseUnrecognizedElement(instance, element, props);
		} catch (Exception ex) {
			LogLog.error("Error in extension content: ", ex);
		}
	}

	/**
	 * Used internally to parse an appender element.
	 */
	protected Appender parseAppender(Element appenderElement) {
		String className = subst(appenderElement.getAttribute(CLASS_ATTR));
		LogLog.debug("Class name: [" + className + ']');
		try {
			Object instance = Loader.loadClass(className).newInstance();
			Appender appender = (Appender) instance;
			PropertySetter propSetter = new PropertySetter(appender);

			appender.setName(subst(appenderElement.getAttribute(NAME_ATTR)));

			NodeList children = appenderElement.getChildNodes();
			final int length = children.getLength();

			for (int loop = 0; loop < length; loop++) {
				Node currentNode = children.item(loop);

				/* We're only interested in Elements */
				if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
					Element currentElement = (Element) currentNode;

					// Parse appender parameters
					if (currentElement.getTagName().equals(PARAM_TAG)) {
						setParameter(currentElement, propSetter);
					}
					// Set appender layout
					else if (currentElement.getTagName().equals(LAYOUT_TAG)) {
						appender.setLayout(parseLayout(currentElement));
					}
					// Add filters
					else if (currentElement.getTagName().equals(FILTER_TAG)) {
						parseFilters(currentElement, appender);
					} else if (currentElement.getTagName().equals(
							ERROR_HANDLER_TAG)) {
						parseErrorHandler(currentElement, appender);
					} else if (currentElement.getTagName().equals(
							APPENDER_REF_TAG)) {
						String refName = subst(currentElement
								.getAttribute(REF_ATTR));
						if (appender instanceof AppenderAttachable) {
							AppenderAttachable aa = (AppenderAttachable) appender;
							LogLog.debug("Attaching appender named [" + refName
									+ "] to appender named ["
									+ appender.getName() + "].");
							aa
									.addAppender(findAppenderByReference(currentElement));
						} else {
							LogLog
									.error("Requesting attachment of appender named ["
											+ refName
											+ "] to appender named ["
											+ appender.getName()
											+ "] which does not implement org.apache.log4j.spi.AppenderAttachable.");
						}
					} else {
						parseUnrecognizedElement(instance, currentElement,
								props);
					}
				}
			}
			propSetter.activate();
			return appender;
		}
		/*
		 * Yes, it's ugly. But all of these exceptions point to the same
		 * problem: we can't create an Appender
		 */
		catch (Exception oops) {
			LogLog.error(
					"Could not create an Appender. Reported error follows.",
					oops);
			return null;
		}
	}

	/**
	 * Used internally to parse an {@link ErrorHandler} element.
	 */
	protected void parseErrorHandler(Element element, Appender appender) {
		ErrorHandler eh = (ErrorHandler) OptionConverter
				.instantiateByClassName(
						subst(element.getAttribute(CLASS_ATTR)),
						org.apache.log4j.spi.ErrorHandler.class, null);

		if (eh != null) {
			eh.setAppender(appender);

			PropertySetter propSetter = new PropertySetter(eh);
			NodeList children = element.getChildNodes();
			final int length = children.getLength();

			for (int loop = 0; loop < length; loop++) {
				Node currentNode = children.item(loop);
				if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
					Element currentElement = (Element) currentNode;
					String tagName = currentElement.getTagName();
					if (tagName.equals(PARAM_TAG)) {
						setParameter(currentElement, propSetter);
					} else if (tagName.equals(APPENDER_REF_TAG)) {
						eh
								.setBackupAppender(findAppenderByReference(currentElement));
					} else if (tagName.equals(LOGGER_REF)) {
						String loggerName = currentElement
								.getAttribute(REF_ATTR);
						Logger logger = (catFactory == null) ? repository
								.getLogger(loggerName) : repository.getLogger(
								loggerName, catFactory);
						eh.setLogger(logger);
					} else if (tagName.equals(ROOT_REF)) {
						Logger root = repository.getRootLogger();
						eh.setLogger(root);
					} else {
						quietParseUnrecognizedElement(eh, currentElement, props);
					}
				}
			}
			propSetter.activate();
			appender.setErrorHandler(eh);
		}
	}

	/**
	 * Used internally to parse a filter element.
	 */
	protected void parseFilters(Element element, Appender appender) {
		String clazz = subst(element.getAttribute(CLASS_ATTR));
		Filter filter = (Filter) OptionConverter.instantiateByClassName(clazz,
				Filter.class, null);

		if (filter != null) {
			PropertySetter propSetter = new PropertySetter(filter);
			NodeList children = element.getChildNodes();
			final int length = children.getLength();

			for (int loop = 0; loop < length; loop++) {
				Node currentNode = children.item(loop);
				if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
					Element currentElement = (Element) currentNode;
					String tagName = currentElement.getTagName();
					if (tagName.equals(PARAM_TAG)) {
						setParameter(currentElement, propSetter);
					} else {
						quietParseUnrecognizedElement(filter, currentElement,
								props);
					}
				}
			}
			propSetter.activate();
			LogLog.debug("Adding filter of type [" + filter.getClass()
					+ "] to appender named [" + appender.getName() + "].");
			appender.addFilter(filter);
		}
	}

	/**
	 * Used internally to parse an category element.
	 */
	protected void parseCategory(Element loggerElement) {
		// Create a new org.apache.log4j.Category object from the <category>
		// element.
		String catName = subst(loggerElement.getAttribute(NAME_ATTR));

		Logger cat;

		String className = subst(loggerElement.getAttribute(CLASS_ATTR));

		if (EMPTY_STR.equals(className)) {
			LogLog.debug("Retreiving an instance of org.apache.log4j.Logger.");
			cat = (catFactory == null)
					? repository.getLogger(catName)
					: repository.getLogger(catName, catFactory);
		} else {
			LogLog.debug("Desired logger sub-class: [" + className + ']');
			try {
				Class clazz = Loader.loadClass(className);
				Method getInstanceMethod = clazz.getMethod("getLogger",
						ONE_STRING_PARAM);
				cat = (Logger) getInstanceMethod.invoke(null,
						new Object[]{catName});
			} catch (Exception oops) {
				LogLog.error("Could not retrieve category [" + catName
						+ "]. Reported error follows.", oops);
				return;
			}
		}

		// Setting up a category needs to be an atomic operation, in order
		// to protect potential log operations while category
		// configuration is in progress.
		synchronized (cat) {
			boolean additivity = OptionConverter.toBoolean(subst(loggerElement
					.getAttribute(ADDITIVITY_ATTR)), true);

			LogLog.debug("Setting [" + cat.getName() + "] additivity to ["
					+ additivity + "].");
			cat.setAdditivity(additivity);
			parseChildrenOfLoggerElement(loggerElement, cat, false);
		}
	}

	/**
	 * Used internally to parse the category factory element.
	 */
	protected void parseCategoryFactory(Element factoryElement) {
		String className = subst(factoryElement.getAttribute(CLASS_ATTR));

		if (EMPTY_STR.equals(className)) {
			LogLog.error("Category Factory tag " + CLASS_ATTR
					+ " attribute not found.");
			LogLog.debug("No Category Factory configured.");
		} else {
			LogLog.debug("Desired category factory: [" + className + ']');
			Object factory = OptionConverter.instantiateByClassName(className,
					LoggerFactory.class, null);
			if (factory instanceof LoggerFactory) {
				catFactory = (LoggerFactory) factory;
			} else {
				LogLog.error("Category Factory class " + className
						+ " does not implement org.apache.log4j.LoggerFactory");
			}
			PropertySetter propSetter = new PropertySetter(factory);

			Element currentElement = null;
			Node currentNode = null;
			NodeList children = factoryElement.getChildNodes();
			final int length = children.getLength();

			for (int loop = 0; loop < length; loop++) {
				currentNode = children.item(loop);
				if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
					currentElement = (Element) currentNode;
					if (currentElement.getTagName().equals(PARAM_TAG)) {
						setParameter(currentElement, propSetter);
					} else {
						quietParseUnrecognizedElement(factory, currentElement,
								props);
					}
				}
			}
		}
	}

	/**
	 * Used internally to parse the roor category element.
	 */
	protected void parseRoot(Element rootElement) {
		Logger root = repository.getRootLogger();
		// category configuration needs to be atomic
		synchronized (root) {
			parseChildrenOfLoggerElement(rootElement, root, true);
		}
	}

	/**
	 * Used internally to parse the children of a category element.
	 */
	protected void parseChildrenOfLoggerElement(Element catElement, Logger cat,
			boolean isRoot) {

		PropertySetter propSetter = new PropertySetter(cat);

		// Remove all existing appenders from cat. They will be
		// reconstructed if need be.
		cat.removeAllAppenders();

		NodeList children = catElement.getChildNodes();
		final int length = children.getLength();

		for (int loop = 0; loop < length; loop++) {
			Node currentNode = children.item(loop);

			if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
				Element currentElement = (Element) currentNode;
				String tagName = currentElement.getTagName();

				if (tagName.equals(APPENDER_REF_TAG)) {
					Element appenderRef = (Element) currentNode;
					Appender appender = findAppenderByReference(appenderRef);
					String refName = subst(appenderRef.getAttribute(REF_ATTR));
					if (appender != null)
						LogLog.debug("Adding appender named [" + refName
								+ "] to category [" + cat.getName() + "].");
					else
						LogLog.debug("Appender named [" + refName
								+ "] not found.");

					cat.addAppender(appender);

				} else if (tagName.equals(LEVEL_TAG)) {
					parseLevel(currentElement, cat, isRoot);
				} else if (tagName.equals(PRIORITY_TAG)) {
					parseLevel(currentElement, cat, isRoot);
				} else if (tagName.equals(PARAM_TAG)) {
					setParameter(currentElement, propSetter);
				} else {
					quietParseUnrecognizedElement(cat, currentElement, props);
				}
			}
		}
		propSetter.activate();
	}

	/**
	 * Used internally to parse a layout element.
	 */
	protected Layout parseLayout(Element layout_element) {
		String className = subst(layout_element.getAttribute(CLASS_ATTR));
		LogLog.debug("Parsing layout of class: \"" + className + "\"");
		try {
			Object instance = Loader.loadClass(className).newInstance();
			Layout layout = (Layout) instance;
			PropertySetter propSetter = new PropertySetter(layout);

			NodeList params = layout_element.getChildNodes();
			final int length = params.getLength();

			for (int loop = 0; loop < length; loop++) {
				Node currentNode = (Node) params.item(loop);
				if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
					Element currentElement = (Element) currentNode;
					String tagName = currentElement.getTagName();
					if (tagName.equals(PARAM_TAG)) {
						setParameter(currentElement, propSetter);
					} else {
						parseUnrecognizedElement(instance, currentElement,
								props);
					}
				}
			}

			propSetter.activate();
			return layout;
		} catch (Exception oops) {
			LogLog.error(
					"Could not create the Layout. Reported error follows.",
					oops);
			return null;
		}
	}

	protected void parseRenderer(Element element) {
		String renderingClass = subst(element
				.getAttribute(RENDERING_CLASS_ATTR));
		String renderedClass = subst(element.getAttribute(RENDERED_CLASS_ATTR));
		if (repository instanceof RendererSupport) {
			RendererMap.addRenderer((RendererSupport) repository,
					renderedClass, renderingClass);
		}
	}

	/**
	 * Used internally to parse a level element.
	 */
	protected void parseLevel(Element element, Logger logger, boolean isRoot) {
		String catName = logger.getName();
		if (isRoot) {
			catName = "root";
		}

		String priStr = subst(element.getAttribute(VALUE_ATTR));
		LogLog.debug("Level value for " + catName + " is  [" + priStr + "].");

		if (INHERITED.equalsIgnoreCase(priStr) || NULL.equalsIgnoreCase(priStr)) {
			if (isRoot) {
				LogLog
						.error("Root level cannot be inherited. Ignoring directive.");
			} else {
				logger.setLevel(null);
			}
		} else {
			String className = subst(element.getAttribute(CLASS_ATTR));
			if (EMPTY_STR.equals(className)) {
				logger.setLevel(OptionConverter.toLevel(priStr, Level.DEBUG));
			} else {
				LogLog.debug("Desired Level sub-class: [" + className + ']');
				try {
					Class clazz = Loader.loadClass(className);
					Method toLevelMethod = clazz.getMethod("toLevel",
							ONE_STRING_PARAM);
					Level pri = (Level) toLevelMethod.invoke(null,
							new Object[]{priStr});
					logger.setLevel(pri);
				} catch (Exception oops) {
					LogLog.error("Could not create level [" + priStr
							+ "]. Reported error follows.", oops);
					return;
				}
			}
		}
		LogLog.debug(catName + " level set to " + logger.getLevel());
	}

	protected void setParameter(Element elem, PropertySetter propSetter) {
		setParameter(elem, propSetter, props);
	}

	/**
	 * Configure log4j using a <code>configuration</code> element as defined in
	 * the log4j.dtd.
	 */
	static public void configure(Element element) {
		DOMConfigurator configurator = new DOMConfigurator();
		configurator.doConfigure(element, LogManager.getLoggerRepository());
	}

	/**
	 * Like {@link #configureAndWatch(String, long)} except that the default
	 * delay as defined by {@link FileWatchdog#DEFAULT_DELAY} is used.
	 * 
	 * @param configFilename
	 *            A log4j configuration file in XML format.
	 */
	static public void configureAndWatch(String configFilename) {
		configureAndWatch(configFilename, FileWatchdog.DEFAULT_DELAY);
	}

	/**
	 * Read the configuration file <code>configFilename</code> if it exists.
	 * Moreover, a thread will be created that will periodically check if
	 * <code>configFilename</code> has been created or modified. The period is
	 * determined by the <code>delay</code> argument. If a change or file
	 * creation is detected, then <code>configFilename</code> is read to
	 * configure log4j.
	 * 
	 * @param configFilename
	 *            A log4j configuration file in XML format.
	 * @param delay
	 *            The delay in milliseconds to wait between each check.
	 */
	static public void configureAndWatch(String configFilename, long delay) {
		XMLWatchdog xdog = new XMLWatchdog(configFilename);
		xdog.setDelay(delay);
		xdog.start();
	}

	private interface ParseAction {
		Document parse(final DocumentBuilder parser) throws SAXException,
				IOException;
	}

	public void doConfigure(final String filename, LoggerRepository repository) {
		ParseAction action = new ParseAction() {
			public Document parse(final DocumentBuilder parser)
					throws SAXException, IOException {
				return parser.parse(new File(filename));
			}
			public String toString() {
				return "file [" + filename + "]";
			}
		};
		doConfigure(action, repository);
	}

	public void doConfigure(final URL url, LoggerRepository repository) {
		ParseAction action = new ParseAction() {
			public Document parse(final DocumentBuilder parser)
					throws SAXException, IOException {
				return parser.parse(url.toString());
			}
			public String toString() {
				return "url [" + url.toString() + "]";
			}
		};
		doConfigure(action, repository);
	}

	/**
	 * Configure log4j by reading in a log4j.dtd compliant XML configuration
	 * file.
	 */
	public void doConfigure(final InputStream inputStream,
			LoggerRepository repository) throws FactoryConfigurationError {
		ParseAction action = new ParseAction() {
			public Document parse(final DocumentBuilder parser)
					throws SAXException, IOException {
				InputSource inputSource = new InputSource(inputStream);
				inputSource.setSystemId("dummy://log4j.dtd");
				return parser.parse(inputSource);
			}
			public String toString() {
				return "input stream [" + inputStream.toString() + "]";
			}
		};
		doConfigure(action, repository);
	}

	/**
	 * Configure log4j by reading in a log4j.dtd compliant XML configuration
	 * file.
	 */
	public void doConfigure(final Reader reader, LoggerRepository repository)
			throws FactoryConfigurationError {
		ParseAction action = new ParseAction() {
			public Document parse(final DocumentBuilder parser)
					throws SAXException, IOException {
				InputSource inputSource = new InputSource(reader);
				inputSource.setSystemId("dummy://log4j.dtd");
				return parser.parse(inputSource);
			}
			public String toString() {
				return "reader [" + reader.toString() + "]";
			}
		};
		doConfigure(action, repository);
	}

	/**
	 * Configure log4j by reading in a log4j.dtd compliant XML configuration
	 * file.
	 */
	protected void doConfigure(final InputSource inputSource,
			LoggerRepository repository) throws FactoryConfigurationError {
		if (inputSource.getSystemId() == null) {
			inputSource.setSystemId("dummy://log4j.dtd");
		}
		ParseAction action = new ParseAction() {
			public Document parse(final DocumentBuilder parser)
					throws SAXException, IOException {
				return parser.parse(inputSource);
			}
			public String toString() {
				return "input source [" + inputSource.toString() + "]";
			}
		};
		doConfigure(action, repository);
	}

	private final void doConfigure(final ParseAction action,
			final LoggerRepository repository) throws FactoryConfigurationError {
		DocumentBuilderFactory dbf = null;
		this.repository = repository;
		try {
			LogLog.debug("System property is :"
					+ OptionConverter.getSystemProperty(dbfKey, null));
			dbf = DocumentBuilderFactory.newInstance();
			LogLog.debug("Standard DocumentBuilderFactory search succeded.");
			LogLog.debug("DocumentBuilderFactory is: "
					+ dbf.getClass().getName());
		} catch (FactoryConfigurationError fce) {
			Exception e = fce.getException();
			LogLog.debug("Could not instantiate a DocumentBuilderFactory.", e);
			throw fce;
		}

		try {
			dbf.setValidating(true);

			DocumentBuilder docBuilder = dbf.newDocumentBuilder();

			docBuilder.setErrorHandler(new SAXErrorHandler());
			docBuilder.setEntityResolver(new Log4jEntityResolver());

			Document doc = action.parse(docBuilder);
			parse(doc.getDocumentElement());
		} catch (Exception e) {
			// I know this is miserable...
			LogLog.error("Could not parse " + action.toString() + ".", e);
		}
	}

	/**
	 * Configure by taking in an DOM element.
	 */
	public void doConfigure(Element element, LoggerRepository repository) {
		this.repository = repository;
		parse(element);
	}

	/**
	 * A static version of {@link #doConfigure(String, LoggerRepository)}.
	 */
	static public void configure(String filename)
			throws FactoryConfigurationError {
		new DOMConfigurator().doConfigure(filename, LogManager
				.getLoggerRepository());
	}

	/**
	 * A static version of {@link #doConfigure(URL, LoggerRepository)}.
	 */
	static public void configure(URL url) throws FactoryConfigurationError {
		new DOMConfigurator()
				.doConfigure(url, LogManager.getLoggerRepository());
	}

	/**
	 * Used internally to configure the log4j framework by parsing a DOM tree of
	 * XML elements based on <a href="doc-files/log4j.dtd">log4j.dtd</a>.
	 */
	protected void parse(Element element) {

		String rootElementName = element.getTagName();

		if (!rootElementName.equals(CONFIGURATION_TAG)) {
			if (rootElementName.equals(OLD_CONFIGURATION_TAG)) {
				LogLog.warn("The <" + OLD_CONFIGURATION_TAG
						+ "> element has been deprecated.");
				LogLog.warn("Use the <" + CONFIGURATION_TAG
						+ "> element instead.");
			} else {
				LogLog.error("DOM element is - not a <" + CONFIGURATION_TAG
						+ "> element.");
				return;
			}
		}

		String debugAttrib = subst(element.getAttribute(INTERNAL_DEBUG_ATTR));

		LogLog.debug("debug attribute= \"" + debugAttrib + "\".");
		// if the log4j.dtd is not specified in the XML file, then the
		// "debug" attribute is returned as the empty string.
		if (!debugAttrib.equals("") && !debugAttrib.equals("null")) {
			LogLog.setInternalDebugging(OptionConverter.toBoolean(debugAttrib,
					true));
		} else {
			LogLog.debug("Ignoring " + INTERNAL_DEBUG_ATTR + " attribute.");
		}

		//
		// reset repository before configuration if reset="true"
		// on configuration element.
		//
		String resetAttrib = subst(element.getAttribute(RESET_ATTR));
		LogLog.debug("reset attribute= \"" + resetAttrib + "\".");
		if (!("".equals(resetAttrib))) {
			if (OptionConverter.toBoolean(resetAttrib, false)) {
				repository.resetConfiguration();
			}
		}

		String confDebug = subst(element.getAttribute(CONFIG_DEBUG_ATTR));
		if (!confDebug.equals("") && !confDebug.equals("null")) {
			LogLog.warn("The \"" + CONFIG_DEBUG_ATTR
					+ "\" attribute is deprecated.");
			LogLog.warn("Use the \"" + INTERNAL_DEBUG_ATTR
					+ "\" attribute instead.");
			LogLog.setInternalDebugging(OptionConverter.toBoolean(confDebug,
					true));
		}

		String thresholdStr = subst(element.getAttribute(THRESHOLD_ATTR));
		LogLog.debug("Threshold =\"" + thresholdStr + "\".");
		if (!"".equals(thresholdStr) && !"null".equals(thresholdStr)) {
			repository.setThreshold(thresholdStr);
		}

		// Hashtable appenderBag = new Hashtable(11);

		/*
		 * Building Appender objects, placing them in a local namespace for
		 * future reference
		 */

		// First configure each category factory under the root element.
		// Category factories need to be configured before any of
		// categories they support.
		//
		String tagName = null;
		Element currentElement = null;
		Node currentNode = null;
		NodeList children = element.getChildNodes();
		final int length = children.getLength();

		for (int loop = 0; loop < length; loop++) {
			currentNode = children.item(loop);
			if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
				currentElement = (Element) currentNode;
				tagName = currentElement.getTagName();

				if (tagName.equals(CATEGORY_FACTORY_TAG)
						|| tagName.equals(LOGGER_FACTORY_TAG)) {
					parseCategoryFactory(currentElement);
				}
			}
		}

		for (int loop = 0; loop < length; loop++) {
			currentNode = children.item(loop);
			if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
				currentElement = (Element) currentNode;
				tagName = currentElement.getTagName();

				if (tagName.equals(CATEGORY) || tagName.equals(LOGGER)) {
					parseCategory(currentElement);
				} else if (tagName.equals(ROOT_TAG)) {
					parseRoot(currentElement);
				} else if (tagName.equals(RENDERER_TAG)) {
					parseRenderer(currentElement);
				} else if (!(tagName.equals(APPENDER_TAG)
						|| tagName.equals(CATEGORY_FACTORY_TAG) || tagName
						.equals(LOGGER_FACTORY_TAG))) {
					quietParseUnrecognizedElement(repository, currentElement,
							props);
				}
			}
		}
	}

	protected String subst(final String value) {
		return subst(value, props);
	}

	/**
	 * Substitutes property value for any references in expression.
	 * 
	 * @param value
	 *            value from configuration file, may contain literal text,
	 *            property references or both
	 * @param props
	 *            properties.
	 * @return evaluated expression, may still contain expressions if unable to
	 *         expand.
	 * @since 1.2.15
	 */
	public static String subst(final String value, final Properties props) {
		try {
			return OptionConverter.substVars(value, props);
		} catch (IllegalArgumentException e) {
			LogLog.warn("Could not perform variable substitution.", e);
			return value;
		}
	}

	/**
	 * Sets a parameter based from configuration file content.
	 * 
	 * @param elem
	 *            param element, may not be null.
	 * @param propSetter
	 *            property setter, may not be null.
	 * @param props
	 *            properties
	 * @since 1.2.15
	 */
	public static void setParameter(final Element elem,
			final PropertySetter propSetter, final Properties props) {
		String name = subst(elem.getAttribute("name"), props);
		String value = (elem.getAttribute("value"));
		value = subst(OptionConverter.convertSpecialChars(value), props);
		propSetter.setProperty(name, value);
	}

	/**
	 * Creates an object and processes any nested param elements but does not
	 * call activateOptions. If the class also supports
	 * UnrecognizedElementParser, the parseUnrecognizedElement method will be
	 * call for any child elements other than param.
	 * 
	 * @param element
	 *            element, may not be null.
	 * @param props
	 *            properties
	 * @param expectedClass
	 *            interface or class expected to be implemented by created class
	 * @return created class or null.
	 * @throws Exception
	 *             thrown if the contain object should be abandoned.
	 * @since 1.2.15
	 */
	public static Object parseElement(final Element element,
			final Properties props, final Class expectedClass) throws Exception {
		String clazz = subst(element.getAttribute("class"), props);
		Object instance = OptionConverter.instantiateByClassName(clazz,
				expectedClass, null);

		if (instance != null) {
			PropertySetter propSetter = new PropertySetter(instance);
			NodeList children = element.getChildNodes();
			final int length = children.getLength();

			for (int loop = 0; loop < length; loop++) {
				Node currentNode = children.item(loop);
				if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
					Element currentElement = (Element) currentNode;
					String tagName = currentElement.getTagName();
					if (tagName.equals("param")) {
						setParameter(currentElement, propSetter, props);
					} else {
						parseUnrecognizedElement(instance, currentElement,
								props);
					}
				}
			}
			return instance;
		}
		return null;
	}

}

class XMLWatchdog extends FileWatchdog {

	XMLWatchdog(String filename) {
		super(filename);
	}

	/**
	 * Call {@link DOMConfigurator#configure(String)} with the
	 * <code>filename</code> to reconfigure log4j.
	 */
	public void doOnChange() {
		new DOMConfigurator().doConfigure(filename, LogManager
				.getLoggerRepository());
	}
}
